Creating a Rich GUI in the IPython Notebook¶
Sep 15, 2014: Based on this thread.
Dec 19, 2016: Updated for this thread
What is the main idea? or…
What would you someone else want to embed/reuse?
What can you make easy to customize?
The controls
The main view
How far can you push the traitlets system?
Things that don’t serialize to JSON might not work very well: arbitrary Python code.
NOTE: this version has been updated to Jupyter Notebook 4.3 and IPyWidgets 5.2.2 The original version is built for the Notebook in IPython 3.0/master.
Installation¶
I highly recommend using conda
via `miniconda
<http://conda.pydata.org/miniconda.html>`__. Once you have conda, you’d run:
[70]:
#!conda install -c conda-forge notebook ipywidgets
If you just did that inside the notebook, you’ll have to restart your notebook server now!
[18]:
from ipywidgets import (
FlexBox, VBox, HBox, HTML, Box, RadioButtons,
FloatText, Dropdown, Checkbox, Image, IntSlider, Button,
)
from traitlets import (
link, Unicode, Float, Int, Enum, Bool,
)
If that just failed, go check `Installation <#Installation>`__ again!
Use `OrderedDict
<https://docs.python.org/2/library/collections.html#collections.OrderedDict>`__ for predictable display of key-value pairs.
[19]:
from collections import OrderedDict
CSS helps keep your code concise, as well as make it easier to extend/override.
[20]:
%%html
<style>
/*
This contents of this would go in a separate CSS file.
Note the namespacing: this is important for two reasons.
1) doesn't pollute the global namespace
2) is _more specific_ than the base styles.
*/
.widget-area .spectroscopy .panel-body{
padding: 0;
}
.widget-area .spectroscopy .widget-numeric-text{
width: 5em;
}
.widget-area .spectroscopy .widget-box.start{
margin-left: 0;
}
.widget-area .spectroscopy .widget-hslider{
width: 12em;
}
</style>
These few classes wrap up some Bootstrap components: these will be more consistent then coding up your own.
NOTE: Bootstrap will not be available in JupyterLab… you’ll still be able to use it, by requiring it yourself, as from a CDN.
[21]:
class PanelTitle(HTML):
def __init__(self, *args, **kwargs):
super(PanelTitle, self).__init__(*args, **kwargs)
self.on_displayed(self.displayed)
def displayed(self, _):
self.add_class("panel-heading panel-title")
class PanelBody(Box):
def __init__(self, *args, **kwargs):
super(PanelBody, self).__init__(*args, **kwargs)
self.on_displayed(self.displayed)
def displayed(self, _):
self.add_class("panel-body")
class ControlPanel(Box):
# A set of related controls, with an optional title, in a box (provided by CSS)
def __init__(self, title=None, *args, **kwargs):
super(ControlPanel, self).__init__(*args, **kwargs)
# add an option title widget
if title is not None:
self.children = [
PanelTitle(value=title),
PanelBody(children=self.children)
]
self.on_displayed(self.displayed)
def displayed(self, _):
self.add_class("panel panel-info")
This notional Spectrogram
shows how one might make a widget that redraws based on the state of its data. By defining its external API, including allowed and default values, in the form of linked traitlets, it can be reused without replumbing any events, while a few simple methods like draw
make sure it is still easy to use in a programmatic way.
[81]:
import re
from datetime import datetime
class Spectrogram(HTML):
"""
A notional "complex widget" that knows how to redraw itself when key properties change.
"""
# Utility
DONT_DRAW = re.compile(r'^(_.+|value|keys|comm|children|visible|parent|log|config|msg_throttle)$')
# Lookup tables: this would be a nice place to add i18n, perhaps
CORRELATION = OrderedDict([(x, x) for x in ["synchronous", "asynchronous", "modulus", "argument"]])
DRAW_MODE = OrderedDict([(x, x) for x in ["color", "black & white", "contour"]])
SPECTRUM_SCALE = OrderedDict([(x, x) for x in ["auto", "manual"]])
SPECTRUM_DIRECTIONS = OrderedDict([(x, x) for x in ["left", "right", "bottom", "top"]])
# pass-through traitlets
correlation = Enum(CORRELATION.values(), default_value=list(CORRELATION.values())[0], sync=True)
draw_mode = Enum(DRAW_MODE.values(), default_value=list(DRAW_MODE.values())[0], sync=True)
spectrum_direction_left = Float(1000, sync=True)
spectrum_direction_right = Float(1000, sync=True)
spectrum_direction_bottom = Float(1000, sync=True)
spectrum_direction_top = Float(1000, sync=True)
spectrum_contours = Int(4, sync=True)
spectrum_zmax = Float(0.0566468618, sync=True)
spectrum_scale = Enum(SPECTRUM_SCALE, default_value=list(SPECTRUM_SCALE.values())[0], sync=True)
axis_x = Float(50, sync=True)
axis_y = Float(50, sync=True)
axis_display = Bool(True, sync=True)
def __init__(self, *args, **kwargs):
"""
Creates a spectrogram
"""
super(Spectrogram, self).__init__(*args, **kwargs)
# self.on_trait_change(lambda name, old, new: self.draw(name, old, new))
self.observe(self.draw)
self.on_displayed(self.displayed)
def displayed(self, _):
self.add_class("col-xs-9")
self.draw()
def draw(self, change=None):
change = change or {}
name = change.get("name")
old = change.get("old")
new = change.get("new")
if name is not None and self.DONT_DRAW.match(name):
return
value = "<h2>Imagine a picture here, drawn with...</h2>"
if name is None:
value += '<div class="alert alert-info">redraw forced at %s!</div>' % (
datetime.now().isoformat(' ')
)
value += "\n".join([
'<p><span class="label label-%s">%s</span> %s</p>' % (
'success' if traitlet == name else 'default',
traitlet,
getattr(self, traitlet)
)
for traitlet in sorted(self.trait_names())
if not self.DONT_DRAW.match(traitlet)
])
self.value = value
The actual GUI. Note that the individual components of the view are responsible for: - creating widgets - linking to the graph widget
[82]:
class Spectroscopy(Box):
"""
An example GUI for a spectroscopy application.
Note that `self.graph` is the owner of all of the "real" data, while this
class handles creating all of the GUI controls and links. This ensures
that the Graph itself remains embeddable and rem
"""
def __init__(self, graph=None, graph_config=None, *args, **kwargs):
self.graph = graph or Spectrogram(**(graph_config or {}))
# Create a GUI
kwargs["orientation"] = 'horizontal'
kwargs["children"] = [
self._controls(),
VBox(children=[
self._actions(),
self.graph
])
]
super(Spectroscopy, self).__init__(*args, **kwargs)
self.on_displayed(self.displayed)
def displayed(self, _):
# namespace and top-level bootstrap
self.add_class("spectroscopy row")
def _actions(self):
redraw = Button(description="Redraw")
redraw.on_click(lambda x: self.graph.draw())
return HBox(children=[redraw])
def _controls(self):
panels = VBox(children=[
HBox(children=[
self._correlation(),
self._draw_mode(),
]),
self._spectrum(),
self._axes()
])
panels.on_displayed(lambda x: panels.add_class("col-xs-3"))
return panels
def _correlation(self):
# create correlation controls. NOTE: should only be called once.
radios = RadioButtons(options=self.graph.CORRELATION)
link((self.graph, "correlation"), (radios, "value"))
return ControlPanel(title="correlation", children=[radios])
def _draw_mode(self):
# create draw mode controls. NOTE: should only be called once.
radios = RadioButtons(options=self.graph.DRAW_MODE)
link((self.graph, "draw_mode"), (radios, "value"))
return ControlPanel(title="draw", children=[radios])
def _spectrum(self):
# create spectrum controls. NOTE: should only be called once.
directions = []
for label in self.graph.SPECTRUM_DIRECTIONS:
direction = FloatText(description=label, value=1000.0)
link((self.graph, "spectrum_direction_" + label), (direction, "value"))
directions.append(direction)
direction_rows = [HBox(children=directions[x::2]) for x in range(2)]
contour = IntSlider(description="contours", min=1)
link((self.graph, "spectrum_contours"), (contour, "value"))
zmax = FloatText(description="z-max", width="100%")
link((self.graph, "spectrum_zmax"), (zmax, "value"))
scale = RadioButtons(description="scale", options=self.graph.SPECTRUM_SCALE)
link((self.graph, "spectrum_scale"), (scale, "value"))
return ControlPanel(title="spectrum",
children=direction_rows + [
contour,
zmax,
scale
]
)
def _axes(self):
# create spectrum controls. NOTE: should only be called once.
axis_x = FloatText(description="X div.")
link((self.graph, "axis_x"), (axis_x, "value"))
axis_y = FloatText(description="Y div.")
link((self.graph, "axis_y"), (axis_y, "value"))
axes = HBox(children=[axis_x, axis_y])
axis_display = Checkbox(description="display")
link((self.graph, "axis_display"), (axis_display, "value"))
return ControlPanel(title="axes",
children=[
axis_display,
axes
]
)
Hooray, everything is defined, now we can try this out!
[83]:
spectrogram = Spectrogram()
spectrogram
Its traits can be updated directly, causing immediate update:
[84]:
spectrogram.axis_display = False
The graph can be passed directly to the interactive GUI, sharing the same data between the two views.
[85]:
gui = Spectroscopy(graph=spectrogram)
gui
[ ]: